package fr.castorflex.openharmony.smoothprogressbar;

import fr.castorflex.openharmony.smoothprogressbar.curvetype.TimeCurveType;
import ohos.agp.components.Component;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.FrameAnimationElement;
import ohos.agp.render.Canvas;
import ohos.agp.render.LinearShader;
import ohos.agp.render.Paint;
import ohos.agp.render.Shader;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.app.dispatcher.TaskDispatcher;

import java.util.Locale;

/**
 * Created by castorflex on 11/10/13.
 */
public class SmoothProgressDrawable extends FrameAnimationElement {
    public interface Callbacks {
        public void onStop();

        public void onStart();
    }

    private float formFact = 1.2f;
    private final static float OFFSET_PER_FRAME = 0.01f;

    private final Rect fBackgroundRect = new Rect();
    private Callbacks mCallbacks;
    private TimeCurveType mCurvetype;
    private Rect mBounds;
    private Paint mPaint;
    private int[] mColors;
    private int mColorsIndex;
    private boolean mRunning;
    private float mCurrentOffset;
    private float mFinishingOffset;
    private int mSeparatorLength;
    private int mSectionsCount;
    private float mSpeed;
    private float mProgressiveStartSpeed;
    private float mProgressiveStopSpeed;
    private boolean mReversed;
    private boolean mNewTurn;
    private boolean mMirrorMode;
    private float mMaxOffset;
    private boolean mFinishing;
    private boolean mProgressiveStartActivated;
    private int mStartSection;
    private int mCurrentSections;
    private float mStrokeWidth;
    private Element mBackgroundDrawable;
    private boolean mUseGradients;
    private int[] mLinearGradientColors;
    private float[] mLinearGradientPositions;

    private RectFloat floatBounds = new RectFloat();
    private Component component;

    private SmoothProgressDrawable(TimeCurveType curvetype,
                                   int sectionsCount,
                                   int separatorLength,
                                   int[] colors,
                                   float strokeWidth,
                                   float speed,
                                   float progressiveStartSpeed,
                                   float progressiveStopSpeed,
                                   boolean reversed,
                                   boolean mirrorMode,
                                   Callbacks callbacks,
                                   boolean progressiveStartActivated,
                                   Element backgroundDrawable,
                                   boolean useGradients) {
        mRunning = false;
        mCurvetype = curvetype;
        mSectionsCount = sectionsCount;
        mStartSection = 0;
        mCurrentSections = mSectionsCount;
        mSeparatorLength = separatorLength;
        mSpeed = speed;
        mProgressiveStartSpeed = progressiveStartSpeed;
        mProgressiveStopSpeed = progressiveStopSpeed;
        mReversed = reversed;
        mColors = colors;
        mColorsIndex = 0;
        mMirrorMode = mirrorMode;
        mFinishing = false;
        mBackgroundDrawable = backgroundDrawable;
        mStrokeWidth = strokeWidth;

        mMaxOffset = 1f / mSectionsCount;

        mPaint = new Paint();
        mPaint.setStrokeWidth(strokeWidth);
        mPaint.setStyle(Paint.Style.STROKE_STYLE);
        mPaint.setDither(false);
        mPaint.setAntiAlias(false);

        mProgressiveStartActivated = progressiveStartActivated;
        mCallbacks = callbacks;

        mUseGradients = useGradients;
        refreshLinearGradientOptions();
    }

    ////////////////////////////////////////////////////////////////////////////
    ///////////////////         SETTERS

    public void setcurvetype(TimeCurveType curvetype, float factor) {
        if (curvetype == null) throw new IllegalArgumentException("curvetype cannot be null");
        mCurvetype = curvetype;
        formFact = factor;
        invalidateSelf();
    }

    public void setComponent(Component component) {
        if (component != null) {
            this.component = component;
        }
    }

    public void invalidateSelf() {
        if (component != null) {
            component.invalidate();
        }
    }

    public void setColors(int[] colors) {
        if (colors == null || colors.length == 0)
            throw new IllegalArgumentException("Colors cannot be null or empty");
        mColorsIndex = 0;
        mColors = colors;
        refreshLinearGradientOptions();
        invalidateSelf();
    }

    public void setColor(int color) {
        setColors(new int[]{color});
    }

    public void setSpeed(float speed) {
        if (speed < 0) throw new IllegalArgumentException("Speed must be >= 0");
        mSpeed = speed;
        invalidateSelf();
    }

    public void setProgressiveStartSpeed(float speed) {
        if (speed < 0) throw new IllegalArgumentException("SpeedProgressiveStart must be >= 0");
        mProgressiveStartSpeed = speed;
        invalidateSelf();
    }

    public void setProgressiveStopSpeed(float speed) {
        if (speed < 0) throw new IllegalArgumentException("SpeedProgressiveStop must be >= 0");
        mProgressiveStopSpeed = speed;
        invalidateSelf();
    }

    public void setSectionsCount(int sectionsCount) {
        if (sectionsCount <= 0) throw new IllegalArgumentException("SectionsCount must be > 0");
        mSectionsCount = sectionsCount;
        mMaxOffset = 1f / mSectionsCount;
        mCurrentOffset %= mMaxOffset;
        refreshLinearGradientOptions();
        invalidateSelf();
    }

    public void setSeparatorLength(int separatorLength) {
        if (separatorLength < 0)
            throw new IllegalArgumentException("SeparatorLength must be >= 0");
        mSeparatorLength = separatorLength;
        invalidateSelf();
    }

    public void setStrokeWidth(float strokeWidth) {
        if (strokeWidth < 0) throw new IllegalArgumentException("The strokeWidth must be >= 0");
        mPaint.setStrokeWidth(strokeWidth);
        invalidateSelf();
    }

    public void setReversed(boolean reversed) {
        if (mReversed == reversed) return;
        mReversed = reversed;
        invalidateSelf();
    }

    public void setMirrorMode(boolean mirrorMode) {
        if (mMirrorMode == mirrorMode) return;
        mMirrorMode = mirrorMode;
        invalidateSelf();
    }

    public void setBackgroundDrawable(Element backgroundDrawable) {
        if (mBackgroundDrawable == backgroundDrawable) return;
        mBackgroundDrawable = backgroundDrawable;
        invalidateSelf();
    }

    public Element getBackgroundDrawable() {
        return mBackgroundDrawable;
    }

    public int[] getColors() {
        return mColors;
    }

    public float getStrokeWidth() {
        return mStrokeWidth;
    }

    public void setProgressiveStartActivated(boolean progressiveStartActivated) {
        mProgressiveStartActivated = progressiveStartActivated;
    }

    public void setUseGradients(boolean useGradients) {
        if (mUseGradients == useGradients) return;

        mUseGradients = useGradients;
        refreshLinearGradientOptions();
        invalidateSelf();
    }

    private void refreshLinearGradientOptions() {
        if (mUseGradients) {
            mLinearGradientColors = new int[mSectionsCount + 2];
            mLinearGradientPositions = new float[mSectionsCount + 2];
        } else {
            mPaint.setShader(null, Paint.ShaderType.LINEAR_SHADER);
            mLinearGradientColors = null;
            mLinearGradientPositions = null;
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    ///////////////////         DRAW

    public void onBoundsChange(Rect bounds) {
        floatBounds.left = bounds.left + mStrokeWidth / 2f + .5f;
        floatBounds.right = bounds.right - mStrokeWidth / 2f - .5f;
        floatBounds.top = bounds.top + mStrokeWidth / 2f + .5f;
        floatBounds.bottom = bounds.bottom - mStrokeWidth / 2f - .5f;
    }

    @Override
    public void drawToCanvas(Canvas canvas) {
        mBounds = new Rect(0, 0, component.getWidth(), component.getHeight());
        onBoundsChange(new Rect(0, 0, component.getWidth(), component.getHeight()));
        canvas.clipRect(floatBounds);

        //new turn
        if (mNewTurn) {
            mColorsIndex = decrementColor(mColorsIndex);
            mNewTurn = false;

            if (isFinishing()) {
                mStartSection++;

                if (mStartSection > mSectionsCount) {
                    stop();
                    return;
                }
            }
            if (mCurrentSections < mSectionsCount) {
                mCurrentSections++;
            }
        }

        if (mUseGradients) {
            prepareGradient();
        }
        drawStrokes(canvas);
    }

    private void prepareGradient() {
        float xSectionWidth = 1f / mSectionsCount;
        int currentIndexColor = mColorsIndex;

        mLinearGradientPositions[0] = 0f;
        mLinearGradientPositions[mLinearGradientPositions.length - 1] = 1f;
        int firstColorIndex = currentIndexColor - 1;
        if (firstColorIndex < 0) firstColorIndex += mColors.length;
        mLinearGradientColors[0] = mColors[firstColorIndex];

        for (int i = 0; i < mSectionsCount; ++i) {
            mLinearGradientPositions[i + 1] = i * xSectionWidth + mCurrentOffset;
            mLinearGradientColors[i + 1] = mColors[currentIndexColor];

            currentIndexColor = (currentIndexColor + 1) % mColors.length;
        }
        mLinearGradientColors[mLinearGradientColors.length - 1] = mColors[currentIndexColor];

        float left = mReversed ? (mMirrorMode ? Math.abs(floatBounds.left - floatBounds.right) / 2 : floatBounds.left) : floatBounds.left;
        float right = mMirrorMode ? (mReversed ? floatBounds.left : Math.abs(floatBounds.left - floatBounds.right) / 2) :
                mBounds.right;
        float top = floatBounds.getCenter().getPointY() - mStrokeWidth / 2;
        float bottom = floatBounds.getCenter().getPointY() + mStrokeWidth / 2;

        Point startPoint = new Point(left, top);
        Point endPoint = new Point(right, bottom);
        Point[] points = new Point[]{startPoint, endPoint};
        LinearShader linearGradient = new LinearShader(points, mLinearGradientPositions, convertIntArrToColorArr(mLinearGradientColors), Shader.TileMode.MIRROR_TILEMODE);
        mPaint.setShader(linearGradient, Paint.ShaderType.LINEAR_SHADER);
    }

    private Color[] convertIntArrToColorArr(int[] linearGradientColors) {
        Color[] colors = new Color[linearGradientColors.length];

        for (int i = 0; i < linearGradientColors.length; i++) {
            colors[i] = new Color(linearGradientColors[i]);
        }

        return colors;
    }

    private void drawStrokes(Canvas canvas) {
        if (mReversed) {
            canvas.translate(mBounds.getWidth(), 0);
            canvas.scale(-1, 1);
        }

        float prevValue = 0f;
        int boundsWidth = mBounds.right;
        if (mMirrorMode) boundsWidth /= 2;
        int width = boundsWidth + mSeparatorLength + mSectionsCount;
        int centerY = mBounds.getCenterY();
        float xSectionWidth = 1f / mSectionsCount;

        float startX;
        float endX;
        float firstX = 0;
        float lastX = 0;
        float prev;
        float end;
        float spaceLength;
        float xOffset;
        float ratioSectionWidth;
        float sectionWidth;
        float drawLength;
        int currentIndexColor = mColorsIndex;

        if (mStartSection == mCurrentSections && mCurrentSections == mSectionsCount) {
            firstX = mBounds.getWidth();
        }

        for (int i = 0; i <= mCurrentSections; ++i) {
            xOffset = xSectionWidth * i + mCurrentOffset;
            prev = Math.max(0f, xOffset - xSectionWidth);
            float position = 0f;

            position = Math.abs(mCurvetype.getCurveType(prev) -
                    mCurvetype.getCurveType(Math.min(xOffset, 1f)));
            ratioSectionWidth = position;
            sectionWidth = (int) (width * ratioSectionWidth);

            if (sectionWidth + prev < width) {
                spaceLength = Math.min(sectionWidth, mSeparatorLength);
            } else {
                spaceLength = 0f;
            }

            drawLength = sectionWidth > spaceLength ? sectionWidth - spaceLength : 0;
            end = prevValue + drawLength;
            if (end > prevValue && i >= mStartSection) {
                float position_curvetype = 0f;
                position_curvetype = mCurvetype.getCurveType(Math.min(mFinishingOffset, 1f));
                float xFinishingOffset = position_curvetype;
                startX = Math.max(xFinishingOffset * width, Math.min(boundsWidth, prevValue));
                endX = Math.min(boundsWidth, end);
                drawLine(canvas, boundsWidth, startX, centerY, endX, centerY, currentIndexColor);
                if (i == mStartSection) { // first loop
                    firstX = startX - mSeparatorLength;
                }
            }
            if (i == mCurrentSections) {
                lastX = prevValue + sectionWidth; //because we want to keep the separator effect
            }

            prevValue = end + spaceLength;
            currentIndexColor = incrementColor(currentIndexColor);
        }
    }

    private void drawLine(Canvas canvas, int canvasWidth, float startX, float startY, float stopX, float stopY, int currentIndexColor) {
        mPaint.setColor(new Color(mColors[currentIndexColor]));

        if (!mMirrorMode) {
            canvas.drawLine(new Point(startX, startY), new Point(stopX, stopY), mPaint);
        } else {
            if (mReversed) {
                canvas.drawLine(new Point(canvasWidth + startX, startY), new Point(canvasWidth + stopX, stopY), mPaint);
                canvas.drawLine(new Point(canvasWidth - startX, startY), new Point(canvasWidth - stopX, stopY), mPaint);
            } else {
                canvas.drawLine(new Point(startX, startY), new Point(stopX, stopY), mPaint);
                canvas.drawLine(new Point(canvasWidth * 2 - startX, startY), new Point(canvasWidth * 2 - stopX, stopY), mPaint);
            }
        }
    }

    private void drawBackgroundIfNeeded(Canvas canvas, float firstX, float lastX) {
        if (mBackgroundDrawable == null) return;

        fBackgroundRect.top = (int) ((mBounds.bottom - mStrokeWidth) / 2);
        fBackgroundRect.bottom = (int) ((mBounds.bottom + mStrokeWidth) / 2);

        fBackgroundRect.left = 0;
        fBackgroundRect.right = mMirrorMode ? mBounds.right / 2 : mBounds.right;
        mBackgroundDrawable.setBounds(fBackgroundRect);

        //draw the background if the animation is over
        if (!isRunning()) {
            if (mMirrorMode) {
                canvas.save();
                canvas.translate(mBounds.right / 2, 0);
                drawBackground(canvas, 0, mBounds.right);
                canvas.scale(-1, 1);
                drawBackground(canvas, 0, mBounds.right);
                canvas.restore();
            } else {
                drawBackground(canvas, 0, mBounds.right);
            }
            return;
        }

        if (!isFinishing() && !isStarting()) return;

        if (firstX > lastX) {
            float temp = firstX;
            firstX = lastX;
            lastX = temp;
        }

        if (firstX > 0) {
            if (mMirrorMode) {
                canvas.save();
                canvas.translate(mBounds.right / 2, 0);
                if (mReversed) {
                    drawBackground(canvas, 0, firstX);
                    canvas.scale(-1, 1);
                    drawBackground(canvas, 0, firstX);
                } else {
                    drawBackground(canvas, mBounds.right / 2 - firstX, mBounds.right / 2);
                    canvas.scale(-1, 1);
                    drawBackground(canvas, mBounds.right / 2 - firstX, mBounds.right / 2);
                }
                canvas.restore();
            } else {
                drawBackground(canvas, 0, firstX);
            }
        }
        if (lastX <= mBounds.right) {
            if (mMirrorMode) {
                canvas.save();
                canvas.translate(mBounds.right / 2, 0);
                if (mReversed) {
                    drawBackground(canvas, lastX, mBounds.right / 2);
                    canvas.scale(-1, 1);
                    drawBackground(canvas, lastX, mBounds.right / 2);
                } else {
                    drawBackground(canvas, 0, mBounds.right / 2 - lastX);
                    canvas.scale(-1, 1);
                    drawBackground(canvas, 0, mBounds.right / 2 - lastX);
                }
                canvas.restore();
            } else {
                drawBackground(canvas, lastX, mBounds.right);
            }
        }
    }


    private void drawBackground(Canvas canvas, float fromX, float toX) {
        int count = canvas.save();
        canvas.clipRect(new RectFloat(fromX, (int) ((mBounds.bottom - mStrokeWidth) / 2),
                toX, (int) ((mBounds.bottom + mStrokeWidth) / 2)));
        mBackgroundDrawable.drawToCanvas(canvas);
        canvas.restoreToCount(count);
    }

    private int incrementColor(int colorIndex) {
        ++colorIndex;
        if (colorIndex >= mColors.length) colorIndex = 0;
        return colorIndex;
    }

    private int decrementColor(int colorIndex) {
        --colorIndex;
        if (colorIndex < 0) colorIndex = mColors.length - 1;
        return colorIndex;
    }

    public void progressiveStart() {
        progressiveStart(0);
    }

    /**
     * Start the animation from a given color.
     *
     * @param index
     */
    public void progressiveStart(int index) {
        resetProgressiveStart(index);
        start();
    }

    private void resetProgressiveStart(int index) {
        checkColorIndex(index);

        mCurrentOffset = 0;
        mFinishing = false;
        mFinishingOffset = 0f;
        mStartSection = 0;
        mCurrentSections = 0;
        mColorsIndex = index;
    }

    private void checkColorIndex(int index) {
        if (index < 0 || index >= mColors.length) {
            throw new IllegalArgumentException(String.format(Locale.US, "Index %d not valid", index));
        }
    }

    /**
     * Finish the animation by animating the remaining sections.
     */
    public void progressiveStop() {
        mFinishing = true;
        mStartSection = 0;
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    ////////////////////////////////////////////////////////////////////////////
    ///////////////////     Listener

    public void setCallbacks(Callbacks callbacks) {
        mCallbacks = callbacks;
    }

    ///////////////////////////////////////////////////////////////////////////
    ///////////////////         Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/
    @Override
    public void start() {
        if (mProgressiveStartActivated) {
            resetProgressiveStart(0);
        }
        TaskDispatcher dispatcher = component.getContext().getUITaskDispatcher();
        dispatcher.delayDispatch(mUpdater, 50);
        mRunning = true;
        invalidateSelf();
    }

    @Override
    public void stop() {
        if (!isRunning()) {
            return;
        }
        if (mCallbacks != null) {
            mCallbacks.onStop();
        }
        mRunning = false;
        unscheduleSelf(mUpdater);
    }

    public void unscheduleSelf(Runnable mUpdater) {
        mRunning = false;
    }

    public boolean isRunning() {
        return mRunning;
    }

    public boolean isStarting() {
        return mCurrentSections < mSectionsCount;
    }

    public boolean isFinishing() {
        return mFinishing;
    }

    private final Runnable mUpdater = new Runnable() {

        @Override
        public void run() {
            if (isFinishing()) {
                mFinishingOffset += (OFFSET_PER_FRAME * mProgressiveStopSpeed);
                mCurrentOffset += (OFFSET_PER_FRAME * mProgressiveStopSpeed);
                if (mFinishingOffset >= 1f) {
                    stop();
                }
            } else if (isStarting()) {
                mCurrentOffset += (OFFSET_PER_FRAME * mProgressiveStartSpeed);
            } else {
                mCurrentOffset += (OFFSET_PER_FRAME * mSpeed);
            }

            if (mCurrentOffset >= mMaxOffset) {
                mNewTurn = true;
                mCurrentOffset -= mMaxOffset;
            }

            if (isRunning()) {
                TaskDispatcher dispatcher = component.getContext().getUITaskDispatcher();
                dispatcher.delayDispatch(mUpdater, 50);
                mRunning = true;
            }
            invalidateSelf();
        }
    };

    ////////////////////////////////////////////////////////////////////////////
    ///////////////////         BUILDER

    /**
     * Builder for SmoothProgressDrawable! You must use it!
     */
    public static class Builder {
        private TimeCurveType mCurveType;
        private int mSectionsCount;
        private int[] mColors;
        private float mSpeed;
        private float mProgressiveStartSpeed;
        private float mProgressiveStopSpeed;
        private boolean mReversed;
        private boolean mMirrorMode;
        private float mStrokeWidth;
        private int mStrokeSeparatorLength;
        private boolean mProgressiveStartActivated;
        private boolean mGenerateBackgroundUsingColors;
        private boolean mGradients;
        private Element mBackgroundDrawableWhenHidden;

        private Callbacks mOnProgressiveStopEndedListener;

        public Builder(Context context) {
            this(context, false);
        }

        public Builder(Context context, boolean editMode) {
            initValues(context, editMode);
        }

        public SmoothProgressDrawable build() {
            if (mGenerateBackgroundUsingColors) {
                mBackgroundDrawableWhenHidden = SmoothProgressBarUtils.generateDrawableWithColors(mColors, mStrokeWidth);
            }
            SmoothProgressDrawable ret = new SmoothProgressDrawable(
                    mCurveType,
                    mSectionsCount,
                    mStrokeSeparatorLength,
                    mColors,
                    mStrokeWidth,
                    mSpeed,
                    mProgressiveStartSpeed,
                    mProgressiveStopSpeed,
                    mReversed,
                    mMirrorMode,
                    mOnProgressiveStopEndedListener,
                    mProgressiveStartActivated,
                    mBackgroundDrawableWhenHidden,
                    mGradients);
            return ret;
        }

        private void initValues(Context context, boolean editMode) {
            mSectionsCount = 4;
            mSectionsCount = 4;
            mSpeed = 1f;
            mReversed = false;
            mProgressiveStartActivated = false;
            mColors = new int[]{0xff33b5e5};
            mStrokeSeparatorLength = 4;
            mStrokeWidth = 4;
            mProgressiveStartSpeed = mSpeed;
            mProgressiveStopSpeed = mSpeed;
            mGradients = false;
        }

        public Builder curvetype(TimeCurveType curvetype) {
            SmoothProgressBarUtils.checkNotNull(curvetype, "curvetype");
            mCurveType = curvetype;
            return this;
        }

        public Builder sectionsCount(int sectionsCount) {
            SmoothProgressBarUtils.checkPositive(sectionsCount, "Sections count");
            mSectionsCount = sectionsCount;
            return this;
        }

        public Builder separatorLength(int separatorLength) {
            SmoothProgressBarUtils.checkPositiveOrZero(separatorLength, "Separator length");
            mStrokeSeparatorLength = separatorLength;
            return this;
        }

        public Builder color(int color) {
            mColors = new int[]{color};
            return this;
        }

        public Builder colors(int[] colors) {
            SmoothProgressBarUtils.checkColors(colors);
            mColors = colors;
            return this;
        }

        public Builder strokeWidth(float width) {
            SmoothProgressBarUtils.checkPositiveOrZero(width, "Width");
            mStrokeWidth = width;
            return this;
        }

        public Builder speed(float speed) {
            SmoothProgressBarUtils.checkSpeed(speed);
            mSpeed = speed;
            return this;
        }

        public Builder progressiveStartSpeed(float progressiveStartSpeed) {
            SmoothProgressBarUtils.checkSpeed(progressiveStartSpeed);
            mProgressiveStartSpeed = progressiveStartSpeed;
            return this;
        }

        public Builder progressiveStopSpeed(float progressiveStopSpeed) {
            SmoothProgressBarUtils.checkSpeed(progressiveStopSpeed);
            mProgressiveStopSpeed = progressiveStopSpeed;
            return this;
        }

        public Builder reversed(boolean reversed) {
            mReversed = reversed;
            return this;
        }

        public Builder mirrorMode(boolean mirrorMode) {
            mMirrorMode = mirrorMode;
            return this;
        }

        public Builder progressiveStart(boolean progressiveStartActivated) {
            mProgressiveStartActivated = progressiveStartActivated;
            return this;
        }

        public Builder callbacks(Callbacks onProgressiveStopEndedListener) {
            mOnProgressiveStopEndedListener = onProgressiveStopEndedListener;
            return this;
        }

        public Builder backgroundDrawable(Element backgroundDrawableWhenHidden) {
            mBackgroundDrawableWhenHidden = backgroundDrawableWhenHidden;
            return this;
        }

        public Builder generateBackgroundUsingColors() {
            mGenerateBackgroundUsingColors = true;
            return this;
        }

        public Builder gradients() {
            return gradients(true);
        }

        public Builder gradients(boolean useGradients) {
            mGradients = useGradients;
            return this;
        }
    }
}
